/*
 * $Id: utf8.c,v 1.8 2005-12-26 07:41:48 bacon Exp $
 */

#include <xp/bas/utf8.h>
#include <xp/bas/assert.h>
#include <xp/bas/errno.h>

/*
 * from RFC 2279 UTF-8, a transformation format of ISO 10646
 *
 *     UCS-4 range (hex.)  UTF-8 octet sequence (binary)
 * 1:2 00000000-0000007F  0xxxxxxx
 * 2:2 00000080-000007FF  110xxxxx 10xxxxxx
 * 3:2 00000800-0000FFFF  1110xxxx 10xxxxxx 10xxxxxx
 * 4:4 00010000-001FFFFF  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
 * inv 00200000-03FFFFFF  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
 * inv 04000000-7FFFFFFF  1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
 */

struct __utf8_t
{
	xp_uint32_t  lower;
	xp_uint32_t  upper;
	xp_uint8_t   fbyte;  /* mask to the first utf8 byte */
	xp_uint8_t   mask;
	xp_size_t    length; /* number of bytes */
};

typedef struct __utf8_t __utf8_t;

static __utf8_t utf8_table[] = 
{
	{0x00000000, 0x0000007F, 0x00, 0x80, 1},
	{0x00000080, 0x000007FF, 0xC0, 0xE0, 2},
	{0x00000800, 0x0000FFFF, 0xE0, 0xF0, 3},
	{0x00010000, 0x001FFFFF, 0xF0, 0xF8, 4},
	{0x00200000, 0x03FFFFFF, 0xF8, 0xFC, 5},
	{0x04000000, 0x7FFFFFFF, 0xFC, 0xFE, 6}
};

xp_size_t xp_unicode_utf8_len (xp_wchar_t ucs)
{
	__utf8_t* cur, * end;

	xp_assert (xp_sizeof(xp_mchar_t) == 1);
	xp_assert (xp_sizeof(xp_wchar_t) >= 2);

	end = utf8_table + xp_countof(utf8_table);
	cur = utf8_table;

	while (cur < end) {
		if (ucs >= cur->lower && ucs <= cur->upper) return cur->length;
		cur++;
	}

	xp_seterrno (XP_EILSEQ);
	return 0; /* invalid character */
}

xp_size_t xp_unicode_to_utf8 (xp_wchar_t ucs, xp_mchar_t* utf8, xp_size_t size)
{
	__utf8_t* cur, * end;
	xp_size_t index;

	xp_assert (utf8 != XP_NULL);
	xp_assert (xp_sizeof(xp_mchar_t) == 1);
	xp_assert (xp_sizeof(xp_wchar_t) >= 2);

	end = utf8_table + xp_countof(utf8_table);
	cur = utf8_table;

	while (cur < end) {
		if (ucs >= cur->lower && ucs <= cur->upper) goto found;
		cur++;
	}

	xp_seterrno (XP_EILSEQ);
	return 0; /* invalid character */

found:
	index = cur->length;
	if (size < index) {
		xp_seterrno (XP_ENOBUFS);
		return 0; /* buffer not big enough */
	}

	while (index > 1) {
		/*
		 * 0x3F: 00111111
		 * 0x80: 10000000
		 */
		utf8[--index] = (ucs & 0x3F) | 0x80;
		ucs >>= 6;
	}

	utf8[0] = ucs | cur->fbyte;
	return cur->length;
}

xp_size_t xp_utf8_to_unicode (
	const xp_mchar_t* utf8, xp_size_t size, xp_wchar_t* ucs)
{
	__utf8_t* cur, * end;
	xp_mchar_t c, t;
	xp_wchar_t w;
	xp_size_t count = 0;

	xp_assert (utf8 != XP_NULL);
	xp_assert (xp_sizeof(xp_mchar_t) == 1);
	xp_assert (xp_sizeof(xp_wchar_t) >= 2);

	end = utf8_table + xp_countof(utf8_table);
	cur = utf8_table;
	
	c = *utf8;
	w = c;

	while (cur < end) {
		count++;

		if ((c & cur->mask) == cur->fbyte) {
			w &= cur->upper;
			if (w < cur->lower) break; /* wrong value */
			*ucs = w;
			return count;
		}

		if (size <= count) break; /* insufficient input */
		utf8++; /* advance to the next character in the sequence */

		t = (*utf8 ^ 0x80) & 0xFF;
		if (t & 0xC0) break;
		w = (w << 6) | t;

		cur++;
	}

	xp_seterrno (XP_EILSEQ);
	return 0; /* error */
}

xp_size_t xp_utf8_sequence_len (xp_mchar_t first)
{
	__utf8_t* cur, * end;

	end = utf8_table + xp_countof(utf8_table);
	cur = utf8_table;

	while (cur < end) {
		if ((first & cur->mask) == cur->fbyte) return cur->length;
		cur++;
	}

	xp_seterrno (XP_EILSEQ);
	return 0; /* error */
}
